package com.brycen.nio.selector;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;

/**
 * selector练习：服务端
 *      server1采用循环读读取客户端消息，只能确保在无并发情况下的读取，无法解决粘包和半包问题
 *      server2采用添加附件bytebuffer的形式，给每个channel绑定一个bytebuffer来读取消息，当缓冲区大小不够时，自动扩充，解决粘包和半包问题
 *
 * 事件有四种类型：
 *      accept：有连接请求时触发的事件
 *      connect：是客户端连接建立后触发的事件
 *      read：可读事件
 *      write：可写事件
 */
public class Server {
    public static void main(String[] args) throws IOException {
        server1();
//        server2();
    }

    /**
     * 案例1：通过循环读取客户端的消息，只能确保在无并发情况下的读取，无法解决粘包和半包问题
     * @throws IOException
     */
    public static void server1() throws IOException {
        //创建ServerSocket通道
        ServerSocketChannel ssChannel = ServerSocketChannel.open();
        //将通道绑定端口
        ssChannel.bind(new InetSocketAddress(9999));
        //配置通道为非阻塞通道
        ssChannel.configureBlocking(false);
        //创建选择器
        Selector selector = Selector.open();
        //将通道与选择器绑定
        //参数1：selector选择器
        //参数2：关注的事件：SelectionKey.OP_ACCEPT：监听注册事件
        //返回值：SelectionKey：就是将来事件发生后，通过他可以知道是什么事件和哪个channel的事件
        ssChannel.register(selector, SelectionKey.OP_ACCEPT);

        //select判断选择器中是否有事件未处理，如果没有则会阻塞；
        //select在事件未处理时不会阻塞，事件发生时要么处理，要么cancel取消，不能置之不理
        while (selector.select()>0){
            //selector.keys()：所有注册的通道
            //selector.selectedKeys()：哪些通道发生了需要处理的事件
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
            while (iterator.hasNext()){
                //获取事件
                SelectionKey sk = iterator.next();
                //判断事件是否是注册事件
                if (sk.isAcceptable()){
                    //接受客户端连接的通道
                    SocketChannel channel = ssChannel.accept();
                    //配置非阻塞通道
                    channel.configureBlocking(false);
                    //注册到选择器上，SelectionKey.OP_READ：监听读事件
                    channel.register(selector,SelectionKey.OP_READ);
                }else if (sk.isReadable()){//判断是否为读事件
                    //通过事件获取当前通道
                    SocketChannel channel = (SocketChannel) sk.channel();
                    ByteBuffer buff = ByteBuffer.allocate(1024);
                    try {
                        /**
                         * 注意：下面的循环读取解决不了粘包和半包的问题，而且上面一开始就分配了1024字节，如果内容没有这么多也会造成空间的浪费
                         * 所以：最有效的方案是和http协议类似，开头固定几个字节放置消息长度，动态分配大小即可
                         */
                        int len;
                        StringBuffer sb = new StringBuffer();
                        while ((len = channel.read(buff)) > 0) {
                            //缓冲区归位，置为读
                            buff.flip();
                            sb.append(new String(buff.array(), 0, buff.remaining()));
                            //清除缓冲区
                            buff.clear();
                        }
                        System.out.println(sb.toString());
                        //客户端正常关闭
                        if (len == -1) {
                            //当len=-1时表示客户端主动断开连接，需要取消该通道的事件
                            //cancel()让selector不在关注该通道，否则会一直产生读事件造成死循环
                            sk.cancel();
                            //关闭通道
                            channel.close();
                        }
                    }catch (Exception e){
                        e.printStackTrace();
                        //客户端异常关闭
                        sk.cancel();
                        //关闭通道
                        channel.close();
                    }
                }
                //移除该事件，否则会重复消费该事件
                iterator.remove();
            }
        }
    }

    /**
     * 案例2：通过在selector中添加附件bytebuffer的形式，给每个channel绑定一个bytebuffer来读取消息，当缓冲区大小不够时，自动扩充，解决粘包和半包问题
     *       但是挨个字符去判断包与包之间的分隔符还是存在效率问题的
     * @throws IOException
     */
    public static void server2() throws IOException {
        //创建ServerSocket通道
        ServerSocketChannel ssChannel = ServerSocketChannel.open();
        //将通道绑定端口
        ssChannel.bind(new InetSocketAddress(9999));
        //配置通道为非阻塞通道
        ssChannel.configureBlocking(false);
        //创建选择器
        Selector selector = Selector.open();
        //将通道与选择器绑定
        //参数1：selector选择器
        //参数2：关注的事件：SelectionKey.OP_ACCEPT：监听注册事件
        //返回值：SelectionKey：就是将来事件发生后，通过他可以知道是什么事件和哪个channel的事件
        ssChannel.register(selector, SelectionKey.OP_ACCEPT);

        //select判断选择器中是否有事件未处理，如果没有则会阻塞；
        //select在事件未处理时不会阻塞，事件发生时要么处理，要么cancel取消，不能置之不理
        while (selector.select()>0){
            //selector.keys()：所有注册的通道
            //selector.selectedKeys()：哪些通道发生了需要处理的事件
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
            while (iterator.hasNext()){
                //获取事件
                SelectionKey sk = iterator.next();
                //判断事件是否是注册事件
                if (sk.isAcceptable()){
                    //接受客户端连接的通道
                    SocketChannel channel = ssChannel.accept();
                    //配置非阻塞通道
                    channel.configureBlocking(false);
                    /**
                        注册到选择器上
                        参数一：selector选择器
                        参数二：SelectionKey.OP_READ：监听读事件
                        参数三：附件，可以和channel绑定在一起
                     */
                    ByteBuffer buff = ByteBuffer.allocate(16);
                    channel.register(selector,SelectionKey.OP_READ,buff);
                }else if (sk.isReadable()){//判断是否为读事件
                    //通过事件获取当前通道
                    SocketChannel channel = (SocketChannel) sk.channel();
                    ByteBuffer buff = (ByteBuffer)sk.attachment();
                    try {
                        /**
                         * 注意：下面没有采用循环读取数据，那么缓冲区不够大时怎么办呢？剩下的数据它会重新触发读事件
                         */
                        int len;
                        if ((len = channel.read(buff)) > 0) {
                            //解决粘包和半包问题
                            split(buff);
                            if (buff.position()==buff.limit()){
                                //在写模式下当position的位置等于limit的位置的时候那么就代表该缓冲区满来，不够存储数据了，需要扩容，原来大小的两倍
                                ByteBuffer newBuff = ByteBuffer.allocate(buff.capacity()*2);
                                //将原来缓冲的剩余数据复制到新的缓冲中去
                                //注意：此时原来的buff需要改为读模式，否则无法将数据put到新的缓冲区中
                                buff.flip();
                                newBuff.put(buff);
                                //将新的扩容重新添加到该channel的附件中去
                                sk.attach(newBuff);
                            }
                        }
                        //客户端正常关闭
                        if (len == -1) {
                            //当len=-1时表示客户端主动断开连接，需要取消该通道的事件
                            //cancel()让selector不在关注该通道，否则会一直产生读事件造成死循环
                            sk.cancel();
                            //关闭通道
                            channel.close();
                        }
                    }catch (Exception e){
                        e.printStackTrace();
                        //客户端异常关闭
                        sk.cancel();
                        //关闭通道
                        channel.close();
                    }
                }
                //移除该事件，否则会重复消费该事件
                iterator.remove();
            }
        }
    }

    private static void split(ByteBuffer source) {
        //切换读模式
        source.flip();
        //循环所有接受到的字节数据
        for (int i = 0; i < source.limit(); i++) {
            //判断是否到达一个数据包的结尾，source.get(i)只是拿到某个角标上的数据，不会移动position
            if (source.get(i)=='\n'){
                //判断这个数据包的长度
                int len = i+1-source.position();
                //分配一个和数据包长度一样的ByteBuffer
                ByteBuffer target = ByteBuffer.allocate(len);
                for (int index = 0; index < len; index++) {
                    target.put(source.get());
                }
                System.out.print(new String(target.array()));
            }
        }
        //压缩数据包，读过的数据会被清除，没有读过的会被保留，和clear不同
        source.compact();
    }
}
